package gen

import (
	"bytes"
	"context"
	"fmt"
	"gorm.io/gen/internal/generate"
	"gorm.io/gen/internal/model"
	tmpl "gorm.io/gen/internal/template"
	"gorm.io/gen/internal/utils/pools"
	"os"
)

// ZormExecute generate zorm code to output path
func (g *Generator) ZormExecute(pk []*model.Column) {
	g.info("Start generating zorm code.")

	if err := g.generateZormModelFile(pk); err != nil {
		g.db.Logger.Error(context.Background(), "generate zorm model struct fail: %s", err)
		panic("generate zorm model struct fail")
	}

	if err := g.generateZormQueryFile(pk); err != nil {
		g.db.Logger.Error(context.Background(), "generate zorm query code fail: %s", err)
		panic("generate zorm query code fail")
	}

	g.info("Generate zorm code done.")
}

// generateZormModelFile generate zorm model structures and save to file
func (g *Generator) generateZormModelFile(pk []*model.Column) error {
	if len(g.models) == 0 {
		return nil
	}

	modelOutPath, err := g.getModelOutputPath()
	if err != nil {
		return err
	}

	if err = os.MkdirAll(modelOutPath, os.ModePerm); err != nil {
		return fmt.Errorf("create zorm model pkg path(%s) fail: %s", modelOutPath, err)
	}

	errChan := make(chan error)
	pool := pools.NewPool(concurrent)
	for _, data := range g.models {
		if data == nil || !data.Generated {
			continue
		}
		zdata := &generate.QueryZormStructMeta{}
		zdata.QueryStructMeta = *data
		zdata.FileName = generate.ChineseToPinyinInitials(zdata.FileName) //将中文转为拼音
		zdata.PyTableName = generate.ChineseToPinyinInitials(zdata.TableName)
		zdata.PyModelStructName = generate.ChineseToPinyinInitials(zdata.ModelStructName)
		pklen := len(pk)
		if pklen > 0 && pklen < 2 {
			zdata.PKColumnName = pk[0].Name()
		} else if pklen > 1 {
			fmt.Printf("Table: %s , has many pk:%v \n", zdata.TableName, pk)
		}
		//zdata.PyFields = zdata.Fields
		zFields := make([]*model.ZormField, 0, len(zdata.Fields))
		for _, v := range zdata.Fields {
			z := &model.ZormField{}
			z.Field = *v
			z.PyName = generate.ChineseToPinyinInitials(v.Name)
			zFields = append(zFields, z)
		}
		zdata.PyFields = zFields
		pool.Wait()
		go func(zdata *generate.QueryZormStructMeta) {
			defer pool.Done()

			var buf bytes.Buffer
			err := render(tmpl.ZORM_Model, &buf, zdata)
			if err != nil {
				errChan <- err
				return
			}

			// zorm 不需要，都定义在 tmpl.ZORM_Model 模板里
			//for _, method := range zdata.ModelMethods {
			//	err = render(tmpl.ZORM_ModelMethod, &buf, method)
			//	if err != nil {
			//		errChan <- err
			//		return
			//	}
			//}

			modelFile := modelOutPath + zdata.FileName + ".gen.go"
			err = g.output(modelFile, buf.Bytes())
			if err != nil {
				errChan <- err
				return
			}

			g.info(fmt.Sprintf("generate zorm model file(table <%s> -> {%s.%s}): %s", data.TableName, data.StructInfo.Package, data.StructInfo.Type, modelFile))
		}(zdata)
	}
	select {
	case err = <-errChan:
		return err
	case <-pool.AsyncWaitAll():
		g.fillModelPkgPath(modelOutPath)
	}
	return nil
}

// generateZormQueryFile generate zorm query code and save to file
func (g *Generator) generateZormQueryFile(pk []*model.Column) (err error) {
	if len(g.Data) == 0 {
		return nil
	}

	if err = os.MkdirAll(g.OutPath, os.ModePerm); err != nil {
		return fmt.Errorf("make dir outpath(%s) fail: %s", g.OutPath, err)
	}

	errChan := make(chan error)
	pool := pools.NewPool(concurrent)
	// generate query code for all struct
	for _, info := range g.Data {
		tc := info.TableComment
		if tc == "VIEW" {
			continue
		}
		fmt.Println("tablecomment:", tc)
		pool.Wait()
		go func(info *genInfo) {
			defer pool.Done()
			err := g.generateZormSingleQueryFile(info, pk)
			if err != nil {
				errChan <- err
			}

			//if g.WithUnitTest {
			//	err = g.generateZormQueryUnitTestFile(info)
			//	if err != nil { // do not panic
			//		g.db.Logger.Error(context.Background(), "generate zorm unit test fail: %s", err)
			//	}
			//}
		}(info)
	}
	select {
	case err = <-errChan:
		return err
	case <-pool.AsyncWaitAll():
	}

	// generate query file
	var buf bytes.Buffer
	err = render(tmpl.ZORM_Header, &buf, map[string]interface{}{
		"Package":        g.queryPkgName,
		"ImportPkgPaths": importZormList.Add(g.importPkgPaths...).Paths(),
	})
	if err != nil {
		return err
	}

	//if g.judgeMode(WithDefaultQuery) {
	//	err = render(tmpl.DefaultQuery, &buf, g)
	//	if err != nil {
	//		return err
	//	}
	//}
	//err = render(tmpl.QueryMethod, &buf, g)
	//if err != nil {
	//	return err
	//}

	err = g.output(g.OutFile, buf.Bytes())
	if err != nil {
		return err
	}
	g.info("generate zorm query file: " + g.OutFile)

	// generate zorm query unit test file
	// TODO 暂时不生成单元测试
	//if g.WithUnitTest {
	//	buf.Reset()
	//
	//	err = render(tmpl.ZORM_Header, &buf, map[string]interface{}{
	//		"Package":        g.queryPkgName,
	//		"ImportPkgPaths": unitTestImportList.Add(g.importPkgPaths...).Paths(),
	//	})
	//	if err != nil {
	//		g.db.Logger.Error(context.Background(), "generate zorm query unit test fail: %s", err)
	//		return nil
	//	}
	//	err = render(tmpl.DIYMethodTestBasic, &buf, nil)
	//	if err != nil {
	//		return err
	//	}
	//	err = render(tmpl.QueryMethodTest, &buf, g)
	//	if err != nil {
	//		g.db.Logger.Error(context.Background(), "generate zorm query unit test fail: %s", err)
	//		return nil
	//	}
	//	fileName := strings.TrimSuffix(g.OutFile, ".go") + "_test.go"
	//	err = g.output(fileName, buf.Bytes())
	//	if err != nil {
	//		g.db.Logger.Error(context.Background(), "generate zorm query unit test fail: %s", err)
	//		return nil
	//	}
	//	g.info("generate zorm unit test file: " + fileName)
	//}

	return nil
}

// ZormGenerateModel catch table info from db, return a BaseStruct
func (g *Generator) ZormGenerateModel(tableName string, opts ...ModelOpt) *generate.QueryZormStructMeta {
	return g.ZormGenerateModelAs(tableName, g.db.Config.NamingStrategy.SchemaName(tableName), opts...)
}

// ZormGenerateModelAs catch table info from db, return a BaseStruct
func (g *Generator) ZormGenerateModelAs(tableName string, modelName string, opts ...ModelOpt) *generate.QueryZormStructMeta {
	meta, pk, err := generate.ZormGetQueryStructMeta(g.db, g.genModelConfig(tableName, modelName, opts))
	if err != nil {
		g.db.Logger.Error(context.Background(), "generate struct from table fail: %s", err)
		panic("generate struct fail")
	}
	if meta == nil {
		g.info(fmt.Sprintf("ignore table <%s>", tableName))
		return nil
	}
	g.models[meta.ModelStructName] = meta
	zmeta := &generate.QueryZormStructMeta{}
	zmeta.QueryStructMeta = *meta
	zmeta.PkColumn = pk
	g.info(fmt.Sprintf("got %d columns from table <%s>", len(meta.Fields), meta.TableName))
	return zmeta
}

// generateZormSingleQueryFile generate query code and save to file
func (g *Generator) generateZormSingleQueryFile(data *genInfo, pk []*model.Column) (err error) {
	var buf bytes.Buffer

	structPkgPath := data.StructInfo.PkgPath
	if structPkgPath == "" {
		structPkgPath = g.modelPkgPath
	}
	err = render(tmpl.ZORM_Header, &buf, map[string]interface{}{
		"Package":        g.queryPkgName,
		"ImportPkgPaths": importZormList.Add(g.importPkgPaths...).Add(structPkgPath).Add(getImportPkgPaths(data)...).Paths(),
	})
	if err != nil {
		return err
	}

	data.QueryStructMeta = data.QueryStructMeta.IfaceMode(g.judgeMode(WithQueryInterface))
	zdata := &generate.QueryZormStructMeta{}
	zdata.QueryStructMeta = *data.QueryStructMeta
	zdata.FileName = generate.ChineseToPinyinInitials(zdata.FileName) //将中文转为拼音
	zdata.PyTableName = generate.ChineseToPinyinInitials(zdata.TableName)
	zdata.PyModelStructName = generate.ChineseToPinyinInitials(zdata.ModelStructName)
	pklen := len(pk)
	if pklen > 0 && pklen < 2 {
		zdata.PKColumnName = pk[0].Name()
		zdata.PKColumnType = pk[0].GetDataType()
	} else if pklen > 1 {
		fmt.Printf("Table: %s , has many pk:%v \n", zdata.TableName, pk)
	}
	//zdata.PyFields = zdata.Fields
	zFields := make([]*model.ZormField, 0, len(zdata.Fields))
	for _, v := range zdata.Fields {
		z := &model.ZormField{}
		z.Field = *v
		z.PyName = generate.ChineseToPinyinInitials(v.Name)
		if zdata.PKColumnName == v.ColumnName {
			zdata.PKColumnName = z.PyName
		}
		zFields = append(zFields, z)
	}
	zdata.PyFields = zFields
	zdata.ModelPkgPath = g.ModelPkgPath
	//structTmpl := tmpl.TableQueryStructWithContext
	//if g.judgeMode(WithoutContext) {
	//	structTmpl = tmpl.TableQueryStruct
	//}
	//err = render(structTmpl, &buf, data.QueryStructMeta)
	//if err != nil {
	//	return err
	//}

	//if g.judgeMode(WithQueryInterface) {
	//	err = render(tmpl.TableQueryIface, &buf, data)
	//	if err != nil {
	//		return err
	//	}
	//}

	//for _, method := range data.Interfaces {
	//	err = render(tmpl.DIYMethod, &buf, method)
	//	if err != nil {
	//		return err
	//	}
	//}

	err = render(tmpl.ZORM_CRUDMethod, &buf, zdata)
	if err != nil {
		return err
	}

	defer g.info(fmt.Sprintf("generate query file: %s%s%s.gen.go", g.OutPath, string(os.PathSeparator), zdata.FileName))
	return g.output(fmt.Sprintf("%s%s%s.gen.go", g.OutPath, string(os.PathSeparator), zdata.FileName), buf.Bytes())
}

// generateZormQueryUnitTestFile generate unit test file for query
func (g *Generator) generateZormQueryUnitTestFile(data *genInfo) (err error) {
	var buf bytes.Buffer

	structPkgPath := data.StructInfo.PkgPath
	if structPkgPath == "" {
		structPkgPath = g.modelPkgPath
	}
	err = render(tmpl.Header, &buf, map[string]interface{}{
		"Package":        g.queryPkgName,
		"ImportPkgPaths": unitTestImportList.Add(structPkgPath).Add(data.ImportPkgPaths...).Paths(),
	})
	if err != nil {
		return err
	}

	err = render(tmpl.CRUDMethodTest, &buf, data.QueryStructMeta)
	if err != nil {
		return err
	}

	for _, method := range data.Interfaces {
		err = render(tmpl.DIYMethodTest, &buf, method)
		if err != nil {
			return err
		}
	}

	defer g.info(fmt.Sprintf("generate unit test file: %s%s%s.gen_test.go", g.OutPath, string(os.PathSeparator), data.FileName))
	return g.output(fmt.Sprintf("%s%s%s.gen_test.go", g.OutPath, string(os.PathSeparator), data.FileName), buf.Bytes())
}

func (g *Generator) SetPyhashFlag(flag bool) {
	generate.SetPyhashFlag(flag)
}
